/*
 * Created on Jun 3, 2005
 */
package com.mgmaps.net;

import java.io.*;
import java.util.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import com.mgmaps.app.APPSettings;
import com.mgmaps.tools.StringTools;
import com.mgmaps.utils.*;

/**
 * This class implements the a downloader thread that downloads queued URL in order,
 * one at a time.
 *
 * @author CristiS
 * @version 1.0
 */
public class Downloader extends Thread {
  
  public static final int AVERAGE_REPLY_SIZE = 1200;
  public static final int AVERAGE_REQUEST_SIZE = 750;
  public static final char DOWNLOAD_SEPARATOR = '|';

  protected ExtQueue q;
  protected boolean quitting;
  protected String currentURL;
  protected Object currentObj;
  // default download handler for enqueuing strings directly
  public static DownloadHandler defaultHandler;
  // cookies
  private static Vector cookies = new Vector();
  
  // headers are not included
  public static int countDownload;
  public static int countUpload;
  public static int countHTTPRequests;
  
  // whether the downloader is active or not
  public boolean active;
  public static Canvas repaintable;
  
  private int redirectCount;
  private static final int MAX_REDIRECTS = 3;
  
  // user agent
  /* private static String userAgent;
  private static final String[] SYSPROPS = {
    "microedition.platform", "microedition.configuration", "microedition.profiles"
  }; */
  
  /**
   * Constructor for Downloader.
   * 
   * @param proxySet proxy settings object
   */
  public Downloader() {
    // create the queue
    q = new ExtQueue();
    // stopper
    // quitting = false;
    // not active
    // active = false;

    /* if (userAgent == null) {
      StringBuffer uaBuf = new StringBuffer("MGMaps/1.3 (");
      uaBuf.append(Props.branch);
      uaBuf.append('-');
      uaBuf.append(Props.version);
      uaBuf.append(')');
      for (int i=0; i<SYSPROPS.length; i++) {
        uaBuf.append(' ');
        uaBuf.append(Props.getSysProp(SYSPROPS[i]));
      }
      userAgent = uaBuf.toString();
    } */
  }
  
  /**
   * Reset the traffic counters.
   */
  public static void resetCounters() {
    countDownload = 0;
    countUpload = 0;
    countHTTPRequests = 0;
  }
  
  /**
   * Run method.
   * 
   * @see java.lang.Runnable#run()
   */
  public void run() {
    // consumer
    Object obj = null;
   
    try {
      while(!isQuitting()) {
        synchronized(this) {
          // is queue empty?
          if (q.isEmpty())
            try {
              wait();
            }
            catch (InterruptedException ie) {
            }
          
          // consume one element
          obj = q.pop();
          if (obj != null) {
            if (obj instanceof Downloadable)
              currentURL = ((Downloadable) obj).getURL();
            else
              currentURL = (String) obj;
            currentObj = obj;
          }
          else {
            currentURL = null;
            currentObj = null;
          }
        }
      
        if (obj != null) {
          try {
            downloadObject(obj);
          }
          catch (Exception e) {
            ExtLogger.handle(e, false, "Exception while downloading");
          }
          currentURL = null;
          currentObj = null;
        }
      }
    }
    catch (Exception e) {
      ExtLogger.handle(e, true, "Exception while downloading");
      currentURL = null;
      currentObj = null;
    }
  }
  
  /**
   * Wrapper for the static method.
   */
  protected void downloadObject(Object obj) {
    int i=0;
    boolean inDownload = false;
    byte[][] data = null;
    String url = null;
    
    try {
      if (obj instanceof Downloadable)
        url = ((Downloadable) obj).getURL();
      else if (obj instanceof String)
        url = (String) obj;
      if (url == null) {
        ExtLogger.debug("URL is null, not downloading anything.");
        return;
      }
      
      // download multiple URLs
      String[] splits = StringTools.split(url, DOWNLOAD_SEPARATOR);
      data = new byte[splits.length][];

      inDownload = true;
      for (i=0; i<splits.length; i++) {
        if (splits[i].length() == 0)
          throw new Exception("Empty URL");
        
        if (!active) {
          active = true;
          // TODO repaint only the download indicator area, this causes flickering
          // repaint for download indicator
          if (repaintable != null)
            repaintable.repaint();
        }

        redirectCount = 0;
        if (obj instanceof HTTPForm)
          data[i] = processURLNoProxy(obj);
        else
          data[i] = processURLNoProxy(splits[i]);
      } // for
      inDownload = false;

      // repaint for download indicator
      active = false;
      if (repaintable != null)
        repaintable.repaint();

      // notify the object that it was downloaded
      if (obj instanceof Downloadable)
        ((Downloadable) obj).notifyDownloaded(data);
      else if (defaultHandler != null)
        defaultHandler.notifyDownloaded(url, data);
    }
    // catch exceptions
    catch (Exception ex) {
      active = false;
      if (repaintable != null)
        repaintable.repaint();
      ExtLogger.handle(ex, false, "Error downloading");
      
      // notify the object about an error only if no map component was downloaded
      if (!inDownload || i == 0 || data == null) {
        if (obj instanceof Downloadable)
          ((Downloadable) obj).notifyError(ex.getMessage());
        else if (defaultHandler != null)
          defaultHandler.notifyError(url, ex.getMessage());
      }
      else {
        if (obj instanceof Downloadable)
          ((Downloadable) obj).notifyDownloaded(data);
        else if (defaultHandler != null)
          defaultHandler.notifyDownloaded(url, data);
      }
      inDownload = false;
    }

    // repaint to display the tile
    // LOW move somewhere else ?
    if (repaintable != null)
      repaintable.repaint();
  }
    
  /**
   * Upload or download a URL without using a proxy.
   * 
   * @param url URL to download
   * @return the data read
   * @throws Exception if an exception occurs
   */
  private byte[] processURLNoProxy(Object o) throws Exception {
    String url = null;
    if (o instanceof String) {
      url = (String) o;
      ExtLogger.debug("Downloading " + url + APPSettings.httpExtension);
    }
    else {
      url = ((HTTPForm) o).getURL();
      ExtLogger.debug("Uploading " + url + APPSettings.httpExtension);
    }

    HttpConnection c = null;
    InputStream is = null;
    OutputStream os = null;
    
    try {
      // open URL connection
      c = (HttpConnection) Connector.open(StringTools.substituteAll(url, "$", "|") + APPSettings.httpExtension);
      c.setRequestMethod((o instanceof HTTPForm) ? ((HTTPForm) o).getMethod() : HttpConnection.GET);
      // no need to close the connection if possible, this improves performance sometimes
      // c.setRequestProperty("Connection", "close");
      
      // set user agent
      if (APPSettings.isBlackberry && APPSettings.platformHandler != null && APPSettings.platformHandler.getUserAgent() != null)
        c.setRequestProperty("User-Agent", APPSettings.platformHandler.getUserAgent());
      else {
        c.setRequestProperty("User-Agent", "Nokia6600/1.0 (4.09.1) SymbianOS/7.0s Series60/2.0 Profile/MIDP-2.0 Configuration/CLDC-1.0");
        // c.setRequestProperty("User-Agent", "Nokia6600/1.0 (4.03.24) SymbianOS/6.1 Series60/2.0 Profile/MIDP-2.0 Configuration/CLDC-1.0");
        // c.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)");
        // c.setRequestProperty("User-Agent", userAgent);
        // set profile
        c.setRequestProperty("Profile", "nds.nokia.com/uaprof/N6600r100.xml");
        c.setRequestProperty("X-WAP-Profile", "nds.nokia.com/uaprof/N6600r100.xml");
      }
      
      // disable the operator transparent proxy
      c.setRequestProperty("Cache-Control", "No-Transform");
      byte[] request = null;
      if (o instanceof HTTPForm) {
        HTTPForm form = (HTTPForm) o;
        // set content type
        c.setRequestProperty("Content-Type", form.getContentType());
        // set content length
        request = form.getRequest();
        c.setRequestProperty("Content-Length", Integer.toString(request.length));
        
        // add headers
        Hashtable headers = form.getAdditionalHeaders();
        for (Enumeration en = headers.keys(); en.hasMoreElements(); ) {
          String k = (String) en.nextElement();
          String v = (String) headers.get(k);
          c.setRequestProperty(k, v);
        }

        // add cookies
        if (form.processCookies)
          addCookies(c, url);
      }
      
      countHTTPRequests++;
      if (o instanceof HTTPForm) {
        os = c.openOutputStream();
        os.write(request);
        os.flush();
        ExtLogger.debug("Uploaded " + request.length + " bytes");
        countUpload += request.length;
      }
      // no output stream open + flush for GET
      // this fixes problems with Windows Mobile
      else {
        os = c.openOutputStream();
        os.flush();
      }
      
      final int respCode = c.getResponseCode();
      
      if (respCode == 404)
        throw new Exception("ERR Not Found");
      else if (respCode == 401)
        throw new Exception("ERR Unauthorized");
      else if (respCode == 403)
        throw new Exception("ERR Forbidden");
      else if (respCode == 405)
        throw new Exception("ERR Method");
      else if (respCode >= 400)
        throw new Exception("Download Error (error code " + respCode + ")");
      
      if (respCode == 301 || respCode == 302 || respCode == 307) {
        final String redirectURL = c.getHeaderField("Location").trim();
        redirectCount++;
        if (redirectCount < MAX_REDIRECTS)
          throw new Exception("ERR Too Many Redirects");
        else
          return processURLNoProxy(redirectURL);
      }
      
      // read cookies and store them
      if (o instanceof HTTPForm)
        if (((HTTPForm) o).processCookies)
          readCookies(c);

      is = c.openInputStream();
      
      // read the URL
      byte[] downloadedData;
      int len = (int) c.getLength();
      if (len > 0) {
        int ch = 0;
        int rd = 0;
        downloadedData = new byte[len];
        while ((rd != len) && (ch != -1)) {
          ch = is.read(downloadedData, rd, len-rd);
          if (ch > 0)
            rd += ch;
        }
        // TODO also count if an error occurs 
        countDownload += rd;
      }
      else {
        int ch;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        for (len=0; (ch = is.read()) != -1; len++)
          bos.write(ch);
        countDownload += len;
        downloadedData = bos.toByteArray();
        bos.close();
      }
      ExtLogger.debug("Downloaded " + len + " bytes");
      
      return downloadedData;
    }
    
    // finally part
    finally {
      if (is != null)
        try {
          is.close();
        }
        catch (IOException ie) {
        }
      if (os != null)
        try {
          os.close();
        }
        catch (IOException ie) {
        }
      if (c != null)
        try {
          c.close();
        }
        catch (IOException ie) {
        }
    }
  }
  
  /**
   * Add all cookies stored for a domain.
   * 
   * @param conn connection to call setRequestProperty for
   * @param url URL that contains the domain
   */
  private static void addCookies(Object conn, String url) throws Exception {
    // extract domain
    String domain = StringTools.extractDomain(url);
    
    // get cookies for this domain
    boolean found = false;
    StringBuffer sb = new StringBuffer();
    int sz = cookies.size();
    for (int i=0; i<sz; i++) {
      String cookie = (String) cookies.elementAt(i);
      int pos = cookie.indexOf(' ');
      if (domain.endsWith(cookie.substring(0, pos))) {
        found = true;
        sb.append(cookie.substring(pos+1));
        sb.append("; ");
      }
    }
    
    // set the cookie header
    if (!found)
      return;
    
    String cookieStr = sb.toString();
    if (conn instanceof HttpConnection)
      ((HttpConnection) conn).setRequestProperty("Cookie", cookieStr);
  }
  
  /**
   * Read cookies from a connection and store them.
   * @param c connection
   */
  private static void readCookies(HttpConnection c) throws Exception {
    // set domain to current host
    String domain = c.getHost();
    if (domain == null)
      return;
    
    for (int k=0; ; k++) {
      // get header key
      String key = c.getHeaderFieldKey(k);
      if (key == null)
        break;
      String value = c.getHeaderField(k);
      // set-cookie header
      if (key.toLowerCase().equals("set-cookie")) {
        // get first name=value pair
        value = value.substring(0, value.indexOf(';'));
        cookies.addElement(domain.toLowerCase() + ' ' + value);
      }
    }
  }
  
  /**
   * Remove the cookies for a domain.
   * @param domain domain to remove cookies for
   */
  public static void removeCookies(String domain) {
    if (domain == null) {
      cookies.removeAllElements();
      return;
    }
    else {
      int sz = cookies.size();
      Vector newCookies = new Vector();
      for (int i=0; i<sz; i++) {
        String cookie = (String) cookies.elementAt(i);
        if (!cookie.startsWith(domain + ' '))
          newCookies.addElement(cookie);
      }
      cookies = newCookies;
    }
  }
  
  /**
   * Synchronized getter for quitting
   * @return true if the thread should quit.
   */
  protected synchronized boolean isQuitting() {
    return quitting;
  }
  
  /**
   * Set quitting to true.
   */
  public synchronized void notifyExit() {
    // clear the queue
    emptyQueue();
    quitting = true;
    // notify the monitor
    notify();
  }
  
  /**
   * Empty the downloader queue.
   */
  public synchronized void emptyQueue() {
    q.clear(); 
  }
  
  /**
   * Enqueue an object for download. By default check it against current object being downloaded.
   * @param o object to download
   */
  public synchronized void enqueue(Downloadable o) {
    enqueue(o, true);
  }
  
  /**
   * Enqueue an object for download.
   * @param o object to download (getURL() gets called)
   * @param dontReplaceQueued true = don't enqueue if the current object being downloaded
   *        is equal to o; false = don't check against currently downloading, replace
   *        queued item if any.
   */
  public synchronized void enqueue(Downloadable o, boolean dontReplaceQueued) {
    String url = o.getURL();
    Object found = null;
    if (url != null)
      found = q.find(o);
    if (!dontReplaceQueued && found != null) {
      q.remove(o);
      found = null;
    }
    if (url == null || !dontReplaceQueued || !o.equals(currentObj) && found == null) {
      ExtLogger.debug("Enqueuing " + (url != null ? url : "(null)"));
      q.push(o);
      notify();
    }
  }
  
  /**
   * Enqueue an object for download.
   */
  public synchronized void enqueue(String o) {
    if (!o.equals(currentObj)) {
      ExtLogger.debug("Enqueuing " + (o != null ? o : "(null)"));
      q.push(o);
      notify();
    }
  }
}
